﻿//AcademicPlanner - course registration planning web app.
//Copyright (C) 2009  Boubacar Diallo

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AcademicPlanner.Data;
using AcademicPlanner.Data.Entities;
using AcademicPlanner.Data.Repositories;
using AcademicPlanner.Domain.Models.Collections;
using AcademicPlanner.Framework.Helpers;
using AcademicPlanner.Framework.Libraries.Validation;
using AcademicPlanner.Domain.Components;

namespace AcademicPlanner.Domain.Models
{
	/// <summary>
	/// AcademicSchedule represents an academic schedule in the application.
	/// </summary>
	/// <since>1.0</since>
	public class AcademicSchedule : Model
	{
		/// <summary>
		/// The minimum value of the <see cref="P:StartYear"/> property of a AcademicSchedule.
		/// </summary>
		/// <since>1.0</since>
		public static readonly double STARTYEAR_MIN_VALUE = 0;

		/// <summary>
		/// The <c>Program</c> this AcademicSchedule is for.
		/// </summary>
		/// <since>1.0</since>
		protected Program program;

		/// <summary>
		/// The <c>AcademicTerm</c> this AcademicSchedule begins in.
		/// </summary>
		/// <since>1.0</since>
		protected AcademicTerm startTerm;

		/// <summary>
		/// The collection of <c>Registration</c>s in this AcademicSchedule.
		/// </summary>
		/// <since>1.0</since>
		protected RegistrationCollection registrations;


		/// <summary>
		/// Gets the data layer repository for this AcademicSchedule's underlying data representation.
		/// </summary>
		/// <since>1.0</since>
		protected override EntityRepository EntityRepo { get { return DomainMaster.DSAS.AcademicScheduleEntityRepo; } }

		/// <summary>
		/// Gets this AcademicSchedule's underlying data representation.
		/// </summary>
		/// <since>1.0</since>
		protected AcademicScheduleEntity Entity { get { return (AcademicScheduleEntity)entity; } }

		/// <summary>
		/// Gets this AcademicSchedule's name.
		/// </summary>
		/// <exception cref="T:AcademicPlanner.Domain.NotAuthorizedException">
		/// Thrown with error code <c>DomainErrorCode.M_NOT_AUTHORIZED_VIEW_ACADEMICSCHEDULE_NAME</c> 
		/// if the user is not authorized to see this property.
		/// </exception>
		/// <since>1.0</since>
		public string Name
		{
			get
			{
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.VIEW_ACADEMICSCHEDULE, this))
					throw new NotAuthorizedException(DomainErrorCode.M_NOT_AUTHORIZED_VIEW_ACADEMICSCHEDULE_NAME);
				return Entity.Name;
			}
		}

		/// <summary>
		/// Gets the year this AcademicSchedule begins in.
		/// </summary>
		/// <exception cref="T:AcademicPlanner.Domain.NotAuthorizedException">
		/// Thrown with error code <c>DomainErrorCode.M_NOT_AUTHORIZED_VIEW_ACADEMICSCHEDULE_STARTYEAR</c> 
		/// if the user is not authorized to see this property.
		/// </exception>
		/// <since>1.0</since>
		public int StartYear
		{
			get
			{
				if (!DomainMaster.Auth.IsUserAuthorized(ACOperationName.VIEW_ACADEMICSCHEDULE, this))
					throw new NotAuthorizedException(DomainErrorCode.M_NOT_AUTHORIZED_VIEW_ACADEMICSCHEDULE_STARTYEAR);
				return Entity.StartPeriodEntity.Year;
			}
		}

		/// <summary>
		/// Gets <c>Program</c> this AcademicSchedule is for.
		/// </summary>
		/// <since>1.0</since>
		public Program Program
		{
			get
			{
				if (program == null && Entity.ProgramEntity != null)
					program = DomainMaster.ModelsFountain.GetProgram(Entity.ProgramEntity);
				return program;
			}
		}

		/// <summary>
		/// Gets the <c>AcademicTerm</c> this AcademicSchedule begins in.
		/// </summary>
		/// <since>1.0</since>
		public AcademicTerm StartTerm
		{
			get
			{
				SetStartTerm();
				return startTerm;
			}
		}

		/// <summary>
		/// Gets the <c>AcademicPlan</c> this AcademicSchedule belongs to.
		/// </summary>
		/// <since>1.0</since>
		public AcademicPlan Plan
		{
			get
			{
				return Program != null ? program.Plan : null;
			}
		}

		/// <summary>
		/// Gets the collection of <c>Registration</c>s in this AcademicSchedule.
		/// </summary>
		/// <since>1.0</since>
		public RegistrationCollection Registrations
		{
			get
			{
				SetRegistrations();
				return registrations;
			}
		}


		/// <summary>
		/// Constructs a new AcademicSchedule.
		/// </summary>
		/// <remarks>
		/// This constructor invokes the matching super constructor.
		/// </remarks>
		/// <since>1.0</since>
		public AcademicSchedule()
			: base()
		{
			
		}

		/// <summary>
		/// Constructs a new AcademicSchedule with the provided <c>AcademicScheduleEntity</c>.
		/// </summary>
		/// <remarks>
		/// This constructor invokes the matching super constructor.
		/// </remarks>
		/// <param name="pEntity">The AcademicSchedule's <c>AcademicScheduleEntity</c>.</param>
		/// <since>1.0</since>
		public AcademicSchedule(AcademicScheduleEntity pEntity) 
			: base(pEntity)
		{
			
		}


		private void SetRegistrations()
		{
			if (registrations == null)
				ResetRegistrations();
		}

		private void ResetRegistrations()
		{
			registrations = new RegistrationCollection(Entity.RegistrationEntities);
		}

		private void SetStartTerm()
		{
			if (startTerm == null)
				ResetStartTerm();
		}

		private void ResetStartTerm()
		{
			startTerm = DomainMaster.ModelsFountain.GetAcademicTerm(Entity.StartPeriodEntity.TermEntity);
		}

		/// <summary>
		/// Gets the sum of <c>Credits</c> of all this AcademicSchedule's <c>Registration</c>s' <c>Course</c>s.
		/// </summary>
		/// <returns>The sum of <c>Credits</c>.</returns>
		/// <since>1.0</since>
		public double GetTotalCredits()
		{
			SetRegistrations();
			return registrations.ToList().Sum(pReg => pReg.Course.Credits);
		}

		/// <summary>
		/// Validates possible values of a AcademicSchedule's properties.
		/// </summary>
		/// <remarks>
		/// <c>String</c> inputs are sanitized first.
		/// </remarks>
		/// <param name="pName">The AcademicSchedule name.</param>
		/// <param name="pProgram">The <c>Program</c> the AcademicSchedule is for.</param>
		/// <param name="pStartYear">The year the AcademicSchedule begins in.</param>
		/// <param name="pStartTerm">The <c>AcademicTerm</c> the AcademicSchedule begins in.</param>
		/// <returns>A Collection of validation errors.</returns>
		/// <seealso cref="M:ValidateName"/>
		/// <seealso cref="M:ValidateStartYear"/>
		/// <seealso cref="M:AcademicPlanner.Framework.Helpers.SecurityHelper.SanitizeInput"/>
		/// <since>1.0</since>
		public static InvalidModelPropertiesErrorCollection Validate(string pName, Program pProgram,
																		int pStartYear, AcademicTerm pStartTerm)
		{
			var rVal = new InvalidModelPropertiesErrorCollection("AcademicSchedule");
			InvalidModelPropertyErrorCode vCode;

			if (!(vCode = ValidateName(SecurityHelper.SanitizeInput(pName)))
					.Equals(InvalidModelPropertyErrorCode.NONE))
				rVal.AddOuter("Name", vCode);

			if (!(vCode = ValidateStartYear(pStartYear))
					.Equals(InvalidModelPropertyErrorCode.NONE))
				rVal.AddOuter("StartYear", vCode);

			if (pProgram == null)
				rVal.AddOuter("Program", InvalidModelPropertyErrorCode.REQUIRED);

			if (pStartTerm == null)
				rVal.AddOuter("StartTerm", InvalidModelPropertyErrorCode.REQUIRED);

			return rVal;
		}

		/// <summary>
		/// Validates a possible value of a AcademicSchedule's name.
		/// </summary>
		/// <remarks>
		/// The value must not be blank.
		/// </remarks>
		/// <param name="pVal">The value to validate.</param>
		/// <returns>A validation error code.</returns>
		/// <seealso cref="M:AcademicPlanner.Framework.Libraries.Validation.StringValidator.IsNotBlank"/>
		/// <since>1.0</since>
		public static InvalidModelPropertyErrorCode ValidateName(string pVal)
		{
			var rVal = InvalidModelPropertyErrorCode.NONE;

			if (!StringValidator.IsNotBlank(pVal))
				rVal = InvalidModelPropertyErrorCode.REQUIRED;

			return rVal;
		}

		/// <summary>
		/// Validates a possible value of a AcademicSchedule's beginning year.
		/// </summary>
		/// <remarks>
		/// The value must greater than <see cref="P:STARTYEAR_MIN_VALUE"/>.
		/// </remarks>
		/// <param name="pVal">The value to validate.</param>
		/// <returns>A validation error code.</returns>
		/// <since>1.0</since>
		public static InvalidModelPropertyErrorCode ValidateStartYear(int pVal)
		{
			var rVal = InvalidModelPropertyErrorCode.NONE;

			if (pVal <= STARTYEAR_MIN_VALUE)
				rVal = InvalidModelPropertyErrorCode.TOO_SMALL;

			return rVal;
		}

		internal void Set(string pName, Program pProgram, int pStartYear, AcademicTerm pStartTerm)
		{
			var vErrs = Validate(pName, pProgram, pStartYear, pStartTerm);

			if (vErrs.OuterErrors.Count > 0)
				throw new InvalidModelPropertiesException(DomainErrorCode.M_INVALID_ACADEMICSCHEDULE_PROPERTIES, vErrs);

			if (Plan != null)
			{
				vErrs = Plan.ValidateSchedule(entity.Id, pName ?? "");
				if (vErrs.OuterErrors.Count > 0)
					throw new InvalidModelPropertiesException(DomainErrorCode.M_INVALID_ACADEMICSCHEDULE_PROPERTIES, vErrs);
			}

			Entity.Name = SecurityHelper.SanitizeInput(pName);
			Entity.StartPeriodEntity.Year = pStartYear;
			Entity.StartPeriodEntity.TermEntity = (AcademicTermEntity)GetOtherModelEntity(pStartTerm);
			Entity.ProgramEntity = (ProgramEntity)GetOtherModelEntity(pProgram);
		}

		internal void RemoveRegistration(int pIndex)
		{
			Entity.RegistrationEntities[pIndex].Trash();
		}

		internal void RemoveRegistration(Registration pRegistration)
		{
			var vEnt = (RegistrationEntity)GetOtherModelEntity(pRegistration);
			var vIndex = Entity.RegistrationEntities.IndexOf(vEnt);

			if (vIndex >= 0)
				RemoveRegistration(vIndex);
		}

		internal void AddRegistration(Registration pRegistration)
		{
			var vRegErrs = ValidateRegistration(pRegistration.EntityId, pRegistration.Year, pRegistration.Term,
												pRegistration.Course);
			var vEnt = (RegistrationEntity)GetOtherModelEntity(pRegistration);

			if (vRegErrs.OuterErrors.Count > 0)
			{
				var vErrs = new InvalidModelPropertiesErrorCollection("AcademicSchedule");

				vErrs.AddInner("Registrations", vRegErrs);
				throw new InvalidModelPropertiesException(DomainErrorCode.M_INVALID_ACADEMICSCHEDULE_REGISTRATIONS_PROPERTIES, vErrs);
			}

			vEnt.ScheduleEntity = Entity;
			Entity.RegistrationEntities.Add(vEnt);
			ResetRegistrations();
		}

		internal InvalidModelPropertiesErrorCollection ValidateRegistration(int pRegistrationId, int pYear, 
																				AcademicTerm pTerm, Course pCourse)
		{
			var rErrs = new InvalidModelPropertiesErrorCollection("Registration");
			var vPrereqs = pCourse.GetPrerequisitesChain();

			SetRegistrations();
			var vRegs = registrations.ToList();

			if (vRegs.Where(pReg => (pReg.Year == pYear && pReg.Term.EntityId == pTerm.EntityId
										&& pReg.Course.EntityId == pCourse.EntityId
										&& pReg.EntityId != pRegistrationId)).Count() > 0)
			{
				rErrs.AddOuter("Course", InvalidModelPropertyErrorCode.NOT_UNIQUE);
			}

			var vPrevRegs = registrations.AnteriorTo(pYear, pTerm.OrderNum).ToList();

			foreach (var vCrse in vPrereqs)
			{
				if (vPrevRegs.Where(pReg => (pReg.Course.EntityId == vCrse.EntityId
											&& pReg.EntityId != pRegistrationId)).Count() <= 0)
				{
					rErrs.AddOuter("Course", InvalidModelPropertyErrorCode.DISALLOWED_VALUE);
				}
			}

			return rErrs;
		}
	}
}
